/*
 * Copyright 2007 Gergely Kis
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.gwthtml.servlet;

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Vector;
import java.util.Map.Entry;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.impl.ElementImpl;
import com.google.gwt.user.client.impl.EventImpl;
import com.gwthtml.util.HTMLParser;

public class GWTContext {


  private static XMLOutputter xmlOutputter;
  
  private static class HistoryTokenList extends ArrayList<String> {
	  
	  private int currentToken;
	  
	  public HistoryTokenList() {
		  super();
		  currentToken = -1;
	  }
	  
	  public String getCurrentToken() {
		  return get(currentToken);
	  }
	  
	  public String getNextToken() {
		  return get(++currentToken);
	  }

	  public String getPrevToken() {
		  return get(--currentToken);
	  }
	  
	  // Creates a new token at the current position + removes the newer 
	  // history entries
	  public void newToken(String token) {
		  add(++currentToken, token);
		  if (size() - 1 > currentToken) {
			  ListIterator<String> iter = listIterator(currentToken + 1);
			  while (iter.hasNext()) {
				  iter.remove();
			  }
		  }
	  }
	  
  }

  static {
	  Format f = Format.getPrettyFormat();
	  f.setOmitDeclaration(true);
	  f.setOmitEncoding(true);
	  xmlOutputter = new XMLOutputter(f);
  }
  
  
	private static ThreadLocal<HttpServletRequest> servletRequest 
						= new ThreadLocal<HttpServletRequest>() {
        	protected synchronized HttpServletRequest initialValue() {
        		return null;
        	}	
      	};
	
	private static ThreadLocal<HttpServletResponse> servletResponse 
						= new ThreadLocal<HttpServletResponse>() {
				        	protected synchronized HttpServletResponse initialValue() {
				        		return null;
				        	}	
				      	};
	
	private static ThreadLocal<List<Cookie>> cookies = new ThreadLocal<List<Cookie>>() {
		protected synchronized List<Cookie> initialValue() {
			return new ArrayList<Cookie>();
		}
	};
	
	private static ThreadLocal<List<Cookie>> cookiesToSend = new ThreadLocal<List<Cookie>>() {
		protected synchronized List<Cookie> initialValue() {
			return new ArrayList<Cookie>();
		}
	};

	private static ThreadLocal<org.jdom.Document> domDocument = new ThreadLocal<org.jdom.Document>() {
		protected synchronized org.jdom.Document initialValue() {
			return null;
		}
	};
	
	private static ThreadLocal<org.jdom.Document> gwtModuleDescriptor = new ThreadLocal<org.jdom.Document>() {
		protected synchronized org.jdom.Document initialValue() {
			return null;
		}
	};

	
	
  private static ThreadLocal<List<EventImpl>> eventQueue = new ThreadLocal<List<EventImpl>>() {
    protected synchronized List<EventImpl> initialValue() {
      return new ArrayList<EventImpl>();
    }
  };

  private static ThreadLocal<HashMap> rootPanels = new ThreadLocal<HashMap>() {
    protected synchronized HashMap initialValue() {
      return new HashMap();
    }
  };

  private static ThreadLocal<List<ElementImpl>> activeElements = new ThreadLocal<List<ElementImpl>>() {
	    protected synchronized List<ElementImpl> initialValue() {
	      return new ArrayList<ElementImpl>();
	    }
	  };
  
	  private static ThreadLocal<HistoryTokenList> historyTokens = new ThreadLocal<HistoryTokenList>() {
		  protected synchronized HistoryTokenList initialValue() {
			  return new HistoryTokenList();
		  }
	  };

	  private static ThreadLocal<Vector> historyListeners = new ThreadLocal<Vector>() {
		  protected synchronized Vector initialValue() {
			  return new Vector();
		  }
	  };
	  
	  
  public static org.jdom.Document getDOMDocument() {
		return domDocument.get();
	}
	
	public static HttpServletRequest getServletRequest() {
		return servletRequest.get();
	}

	public static HttpServletResponse getServletResponse() {
		return servletResponse.get();
	}
	
	public static String getCookieValue(String key) {
		for (Cookie c : cookies.get()) {
			if (c.getName().equals(key)) {
				return c.getValue();
			}
		}
		return "";
	}

	public static void setCookie(String name, String value, long expires,
		      String domain, String path, boolean secure) {
		
		for (Cookie c : cookies.get()) {
			if (c.getName().equals(name)) {
				// FIXME: Should we also check for domain and path?
				// change an existing cookie
				c.setValue(value);
				c.setMaxAge((int)expires);
				c.setDomain(domain);
				c.setPath(path);
				c.setSecure(secure);
				cookiesToSend.get().add(c);
				return ;
			}
		}
		
		Cookie c = new Cookie(name, value);
		c.setDomain(domain);
		c.setPath(path);
		c.setSecure(secure);
		c.setMaxAge((int) expires);
		cookiesToSend.get().add(c);
		cookies.get().add(c);		
	}
	
	public static void initContext(HttpServletRequest request, HttpServletResponse response) {
		assert (request != null);
		assert (response != null);
		setServletRequest(request);
		setServletResponse(response);
		initCookies();
		loadSession();
		initEventQueue();
	}

  public static void execute(HttpServlet servlet) {
    if (domDocument.get() == null) {
      // This is the first access of the module in the session.
      String gwtModuleName = servlet.getInitParameter("GWT-ModuleName");
      gwtModuleDescriptor.set(parseGWTModuleDescription(servlet, gwtModuleName));
      initDocumentFromPlaceholder(getGWTPlaceholderPath(gwtModuleDescriptor.get(), servlet));
      EntryPoint entryPoint = instantiateEntryPoint();
      entryPoint.onModuleLoad();
    }
    
    decodeAndEmitEvents();
    
    Iterator<EventImpl> iter = eventQueue.get().iterator();
    while (iter.hasNext()) {
      EventImpl evt = iter.next();
      DOM.dispatchEvent(evt);
    }
  }
  

  private static EntryPoint instantiateEntryPoint() {
	  Document desc = gwtModuleDescriptor.get();
	  if (desc != null) {
		  Element module = desc.getRootElement();
		  Element entrypoint = module.getChild("entry-point");
		  String className = entrypoint.getAttributeValue("class");
		  try {
			return (EntryPoint) Class.forName(className).newInstance();
		} catch (Exception e) {
			// Return a null TODO - Exception handling
		}
	  }
	  return null;
 }

  public static String getGWTPlaceholderPath(Document desc, HttpServlet servlet) {
	if (desc != null) {
		Element module = desc.getRootElement();
		Element entrypoint = module.getChild("entry-point");
		String className = entrypoint.getAttributeValue("class");
		int lastDot = className.lastIndexOf(".");		
		return servlet.getServletContext().getRealPath("/"+className.substring(lastDot+1)+".html");
	}
	return null;
  }

  private static String getGWTStylesheetPath() {
	  Document desc = gwtModuleDescriptor.get();
	  if (desc != null) {
		  Element module = desc.getRootElement();
		  Element stylesheet = module.getChild("stylesheet");
		  if (stylesheet != null) {
			  return stylesheet.getAttributeValue("src");
		  }
	  }
	  return null;
  }

  
  public static Document parseGWTModuleDescription(HttpServlet servlet, String moduleName) {
	try {
		StringBuffer descriptorPath = new StringBuffer();
		descriptorPath.append(moduleName.replaceAll("\\.", "/"));
		descriptorPath.insert(0, "/");
		descriptorPath.append(".gwt.xml");
		InputStream is = servlet.getClass().getResourceAsStream(descriptorPath.toString());
		SAXBuilder builder = new SAXBuilder();
		return builder.build(is);
	} catch (Exception ex) {
		throw new RuntimeException(ex);
	}
}

private static void decodeAndEmitEvents() {
	HttpServletRequest req = servletRequest.get();

	
	// Check for history changes
	String requestedToken = req.getParameter("history");
	if (requestedToken != null) {
		if (!requestedToken.equals(getCurrentHistoryItem())) {
			History.newItem(requestedToken);
		}
	}
	
	String clickStr = req.getParameter("click");
	if (clickStr != null) {
		try {
			int click = new Integer(clickStr).intValue();
			for (ElementImpl e : activeElements.get()) {
				if (e.hashCode() == click) {
					EventImpl evt = new EventImpl(Event.ONCLICK | Event.BUTTON_LEFT);
					evt.setTarget(e);
					eventQueue.get().add(evt);
					break;
				}
			}
		}
		catch (NumberFormatException nex) {
			// Invalid input data
		}
	}
	
	String formIdStr = req.getParameter("_formId");
	if (formIdStr != null)
	{
		try {
			int formId = new Integer(formIdStr).intValue();
			for (ElementImpl e : activeElements.get()) {
				if (e.hashCode() == formId) {
					decodeAndEmitFormEvents(e);
				}
			}
		} catch (NumberFormatException nex) {
			// Invalid input data
		}
	}
  }

private static void decodeAndEmitFormEvents(ElementImpl e) {
	HttpServletRequest req = servletRequest.get();
	Iterator<Entry<String,String>> paramIter = req.getParameterMap().entrySet().iterator();
	while (paramIter.hasNext()) {
		Entry<String,String> entry = paramIter.next();
		ElementImpl elem = findElementByName(e, entry.getKey());
		if (elem.getName().equals("input")) {
			// We have an input event
			String type = elem.getAttributeValue("type");
			if (type == null) {
				continue;
			}
			if (type.equals("checkbox")) {
				// No idea yet...
			}
		}		
	}
}

private static ElementImpl findElementByName(ElementImpl current, String name) {
	String nameValue = current.getAttributeValue("name");
	if (nameValue != null) {
	  if (nameValue.equals(name)) {
		return current;
	  }
	}
	
	List childList = current.getChildren();
	Iterator iter = childList.iterator();
	while (iter.hasNext()) {
		ElementImpl element = (ElementImpl) iter.next(); 
		ElementImpl found = findElementByName(element, name);
		if (found != null) {
			return found;
		}
	}
	return null;
}

public static void commitContext(boolean requestDone) {
	if (!requestDone) {
		commitCookies();
		serializeDocument();
	}
    saveSession();
    setServletRequest(null);
    setServletResponse(null);
  }
	
	private static void serializeDocument() {
    try {
      xmlOutputter.output(domDocument.get(), servletResponse.get().getWriter());
      servletResponse.get().setContentType("text/html");
    } catch (IOException e) {
      // TODO: More useful error handling
    }
    
  }
  
  private static void initDocumentFromPlaceholder(String placeholderPath) {
    try {
		Document doc = HTMLParser.parseHtmlDocument(new FileReader(placeholderPath));
		stripGWTBootstrap(doc.getRootElement());
		String styleSheetPath = getGWTStylesheetPath();
		if (doc != null && styleSheetPath != null) {
			Element head = doc.getRootElement().getChild("head");
			if (head != null) {
				Element link = new Element("link");
				link.setAttribute("href", styleSheetPath);
				link.setAttribute("type", "text/css");
				link.setAttribute("rel", "stylesheet");
				head.addContent(link); 
			}
		}
		domDocument.set(doc);
    } catch (FileNotFoundException e) {
		throw new RuntimeException(e);
	}
  }

  private static boolean isGWTBootstrapElement(Element root) {
		String name = root.getName();
		if (name.equals("script")) {
			String src = root.getAttributeValue("src");
			if (src != null) {
				if (src.equals("gwt.js")) {
					return true;
				}
			}
		}
		return false;
  }
  
  private static boolean isGWTHistoryFrame(Element root) {
		String name = root.getName();
		if (name.equals("iframe")) {
			String src = root.getAttributeValue("id");
			if (src != null) {
				if (src.equals("__gwt_historyFrame")) {
					return true;
				}
			}
		}
		return false;
}

  
  private static void stripGWTBootstrap(Element root) {
		
		List childList = root.getChildren();
		Iterator iter = childList.iterator();
		while (iter.hasNext()) {
			ElementImpl element = (ElementImpl) iter.next();
			if (isGWTBootstrapElement(element) || isGWTHistoryFrame(element)) {
				iter.remove();
			} else {
				stripGWTBootstrap(element);
			}
		}
  }
 
private static void saveSession() {
    HttpSession session = servletRequest.get().getSession();
    session.setAttribute("GWT-Document", domDocument.get());
    session.setAttribute("GWT-RootPanels", rootPanels.get());    
    session.setAttribute("GWT-ActiveElements", activeElements.get());
    session.setAttribute("GWT-ModuleDescriptor", gwtModuleDescriptor.get());
    session.setAttribute("GWT-HistoryTokens", historyTokens.get());
    session.setAttribute("GWT-HistoryListeners", historyListeners.get());
  }

  private static void loadSession() {
    HttpSession session = servletRequest.get().getSession();
    domDocument.set( (Document) session.getAttribute("GWT-Document"));
    gwtModuleDescriptor.set( (Document) session.getAttribute("GWT-ModuleDescriptor"));

    HashMap rp = (HashMap) session.getAttribute("GWT-RootPanels");
    if (rp != null) {
      rootPanels.set(rp);
    }

    List<ElementImpl> activeElems = (List<ElementImpl>) session.getAttribute("GWT-ActiveElements");
    if (activeElems != null) {
    	activeElements.set(activeElems);
    }
    
    HistoryTokenList hist = (HistoryTokenList) session.getAttribute("GWT-HistoryTokens");
    if (hist != null) {
    	historyTokens.set(hist);
    }

    Vector histListeners = (Vector) session.getAttribute("GWT-HistoryListeners");
    if (histListeners != null) {
    	historyListeners.set(histListeners);
    }

  }

  private static void initEventQueue() {
    eventQueue.get().clear();    
  }

	private static void  setServletRequest(HttpServletRequest request) {
		servletRequest.set(request);
	}
	
	private static void initCookies() {
		cookies.get().clear();
		cookiesToSend.get().clear();
		Cookie[] cs = servletRequest.get().getCookies();
		if (cs != null) {
			for (Cookie c : cs) {
				cookies.get().add(c);
			}
		}
	}

	private static void  setServletResponse(HttpServletResponse response) {
		servletResponse.set(response);
	}
	

	private static void commitCookies() {
		for (Cookie c : cookiesToSend.get()) {
			servletResponse.get().addCookie(c);
		}
	}

	public static void setDOMDocument(org.jdom.Document doc) {
		domDocument.set(doc);
	}

  public static HashMap rootPanels() {
    return rootPanels.get();
  }

  public static XMLOutputter getXMLOutputter() {
    return xmlOutputter;
  }

	public static void emitEvent(EventImpl evt) {
		eventQueue.get().add(evt);

	}

	public static void addActiveElement(ElementImpl element) {
		activeElements.get().add(element);
	}
	
	public static void removeActiveElement(ElementImpl element) {
		activeElements.get().remove(element);
	}

	public static String getPrevHistoryToken() {
		return historyTokens.get().getPrevToken();
	}

	public static void newHistoryItem(String historyToken) {
		historyTokens.get().newToken(historyToken);
	}

	public static String getCurrentHistoryItem() {
		try {
			return historyTokens.get().getCurrentToken();
		} catch (IndexOutOfBoundsException e) {
			return "";
		}
	}

	public static String getNextHistoryToken() {
		return historyTokens.get().getNextToken();
	}

	public static Vector historyListeners() {
		
		return historyListeners.get();
	}

}
 